package com.owg.jigsaw;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Queue;

import com.owg.jigsaw.contour.Contour;
import com.owg.jigsaw.hierarchy.TypeHierarchy;
import com.owg.jigsaw.javafx.FXArrow;
import com.owg.jigsaw.javafx.FXBlock;
import com.owg.jigsaw.javafx.FXUtil;
import com.owg.jigsaw.javafx.fxml.WPaneSpawner;
import com.owg.jigsaw.slots.DropTargetHighlighter;
import com.owg.jigsaw.slots.Slot;
import com.owg.jigsaw.slots.DropTargetHighlighter.Highlight;

import owg.util.Calc;
import owg.util.data.PField;
import owg.util.data.PFieldList;
import owg.util.data.PFieldOptional;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;

public class WRoot implements WNode, WSingleFieldContainer<WNode>, EventHandler<MouseEvent>
{	
	protected interface ReturnTarget
	{
		public void returnBlock(WBlock b);
	}
	
	protected final TypeHierarchy typeHierarchy;
	protected Pane rootPane;
	protected Parent contentPane;
	protected PFieldList<WNode> children = new PFieldList<WNode>(this, WNode.class, 0, -1);
	protected HashMap<WPointerBlock, FXArrow> connectorArrows = new HashMap<>();
	
	protected ReturnTarget dragCancel;
	public final PFieldOptional<WBlock> dragging = new PFieldOptional<WBlock>(this, WBlock.class);
	protected Point2D dragOffset = null;
	
	protected DropTargetHighlighter[] highlights = null;
	protected DropTargetHighlighter.Highlight currentHighlight = null;
	
	public WRoot(TypeHierarchy typeHierarchy)
	{
		this.typeHierarchy = typeHierarchy;
	}
	
	public void add(WPane p)
	{
		assert !children.contains(p) : "Duplicate add: "+p;
		Pane ui = p.getUI();
		//Note: listeners on root pane is insufficient because
		//there is a discontinuity in the hierarchy at tabpanes.
		//This is terrible design, but we just have to deal.
		ui.onMousePressedProperty().set(this);
		ui.onMouseDraggedProperty().set(this);
		ui.onMouseReleasedProperty().set(this);
		children.add(p);
	}
	
	public void installContentPane(Parent p)
	{
		this.contentPane = p;
		fxmlSearch(p);
	}
	/**Search p's subtree for instances of {@link WPaneSpawner}. If they are found, their {@link WPane}s are spawned and added to this root.*/
	private void fxmlSearch(Node p)
	{
		if(p instanceof WPaneSpawner)
		{
			add(((WPaneSpawner) p).getWPane(this));
		}
		else if(p instanceof Parent)//Note: WPanes in WPanes not supported..
		{
			for(Node c : ((Parent) p).getChildrenUnmodifiable())
			{
				fxmlSearch(c);
			}
			
			((Parent) p).getChildrenUnmodifiable().addListener(new ListChangeListener<Node>()
			{
				@Override
				public void onChanged(
					javafx.collections.ListChangeListener.Change<? extends Node> c)
				{
					while(c.next())
					{
						if(c.wasAdded())
						{
							for(Node n : c.getAddedSubList())
								fxmlSearch(n);
						}
					}
				}	
			});
		}
	}

	protected Pane createRootPane()
	{
		Pane rootPane;
		if(contentPane == null)
			throw new IllegalStateException("WRoot cannot automatically create content pane: Add one using installContentPane() first!");
		rootPane = new Pane(){@Override
			protected void layoutChildren()
		{
			//Only contentPane has a fixed location on this pane
			for(Node c : getChildren())
			{
				if(c == contentPane)
					contentPane.resizeRelocate(0, 0, getWidth(), getHeight());
				else
					c.autosize();
			}
		}};
		rootPane.getChildren().add(contentPane);
		
		//Note: Mouse listeners are added to both rootPane and child panes.
		//This should be redundant. However:
		//1. There is a discontinuity at tabpanes which prevents events
		//   from being delivered through it.
		//2. The child panes sometimes become insensitive to user input if
		//   their layout changes as a result of a user action. 
		rootPane.onMousePressedProperty().set(this);
		rootPane.onMouseDraggedProperty().set(this);
		rootPane.onMouseReleasedProperty().set(this);
		
		return rootPane;
	}
	public Parent getContentPane()
	{
		return contentPane;
	}
	
	@Override
	public Pane getUI()
	{
		if(rootPane == null)
			rootPane = createRootPane();
		return rootPane;
	}
	
	public void dragStart(WBlock b, double xOffset, double yOffset, double absX, double absY, boolean lineConnect)
	{
		WBlock origB = b;
		if(dragging.isEmpty())
		{
			try 
			{
				if(lineConnect && b.isPaletteBlock())
					throw new IllegalArgumentException("Palette block: "+origB+" cannot have line connections...");
				
				
				if(lineConnect)
				{
					b = b.makePointerBlock();
					if(b == null)
						throw new IllegalArgumentException(origB+" cannot have line connections...");
					dragCancel = (blok -> {blok.dispose();});
				}
				else if(b.isPaletteBlock())
				{
					b = b.copy(true);
					dragCancel = (blok -> {blok.dispose();});
				}
				else
				{
					final WSingleFieldContainer<WBlock> retContainer = b.blockContainer().get();
					final int retIndex = retContainer.getChildren().indexOf(b);
					final int retX = b.getX();
					final int retY = b.getY();
					dragCancel = (blok -> 
					{
						try 
						{
							retContainer.getChildren().insert(retIndex, blok);
							if(!(retContainer instanceof Slot))
								blok.setLocation(retX, retY);
						}
						catch(IllegalArgumentException e)
						{
							System.err.println("Warning: Block "+blok+" could not be returned to its original location!");
							//The block could not be returned. This is likely an error in the application, but we must recover.
							e.printStackTrace();
							blok.dispose();
						}
					});
					retContainer.getChildren().remove(b);
				}
				
				dragOffset = new Point2D(xOffset, yOffset);
				//Need to add before getting UI(why?)
				dragging.set(b);
				assert b.getParent() == this : "Dropped block has the wrong parent: "+b.getParent();
				Pane ui = b.getUI();
				//The z-ordering scheme is fragile. 
				//The block ui should be in front of content pane and highlights(added below), but behind arrows. 
				rootPane.getChildren().add(1, ui);
				ui.requestFocus();
				b.setLocation((int)(absX-dragOffset.getX()), (int) (absY-dragOffset.getY()));
				
				Slot[] slots = getTargetSlots();
				highlights = new DropTargetHighlighter[slots.length];
				for(int i = 0; i<slots.length; i++)
				{
					highlights[i] = new DropTargetHighlighter(this, slots[i], b);
				}
				selectHighlight(absX-dragOffset.getX(), absY-dragOffset.getY());
			}
			catch(IllegalArgumentException e)
			{
				if(!dragging.isEmpty())
					dragging.remove();
				dragOffset = null;
				dragCancel = null;
				highlights = null;
				currentHighlight = null;
					
				System.err.println("Can't do this because "+e.getMessage());
			}
		}
	}
	private Slot[] getTargetSlots()
	{
		Queue<Slot> sq = new LinkedList<>();
		putTargetSlots(sq);
		return sq.toArray(new Slot[sq.size()]);
	}
	@Override
	public void putTargetSlots(Queue<Slot> sq)
	{
		for(WNode c : children)
		{
			if(FXUtil.isVisible(c.getUI()))
				c.putTargetSlots(sq);
		}
	}
	
	public void drag(double absX, double absY)
	{
		if(!dragging.isEmpty())
		{
			dragging.get().setLocation((int)(absX-dragOffset.getX()), (int) (absY-dragOffset.getY()));
			selectHighlight(absX-dragOffset.getX(), absY-dragOffset.getY());
		}
	}
	private void selectHighlight(double x, double y)
	{
		final double SNAP_THRESHOLD = 32;
		double hTabX, hTabY, vTabX, vTabY;
		Contour contour = dragging.get().tabType().get().getContour();
		hTabX = x-contour.getWidth();
		hTabY = y+FXBlock.TAB_OFFSET;
		vTabX = x+FXBlock.TAB_OFFSET;
		vTabY = y-contour.getWidth();
		
		Highlight closest = null;
		double minDist = Double.MAX_VALUE;
		for(DropTargetHighlighter highlighter : highlights)
		{
			double tx, ty;
			if(highlighter.isHorizontal())
			{
				tx = hTabX;
				ty = hTabY;
			}
			else
			{
				tx = vTabX;
				ty = vTabY;
			}
			Highlight h = highlighter.getClosestHighlight(tx, ty);
			double dist = Calc.manDist(h.getX(), h.getY(), tx, ty);
			if(dist < minDist)
			{
				minDist = dist;
				closest = h;
			}
		}
		Highlight choice;
		if(minDist < SNAP_THRESHOLD)
			choice = closest;
		else
			choice = null;
		
		if(choice != currentHighlight)
		{
			if(currentHighlight != null)
				currentHighlight.setHover(false);
			currentHighlight = choice;
			if(currentHighlight != null)
				currentHighlight.setHover(true);
		}
	}
	
	public void dragEnd(double absX, double absY)
	{
		if(!dragging.isEmpty())
		{
			//Remove the UI from the root pane
			rootPane.getChildren().remove(dragging.get().getUI());
			//Force repaint because JavaFX is an ass
			FXUtil.forceRepaint(rootPane);
			
			//Remember the value BEFORE removing it
			WBlock dragBlock = dragging.get();
			//Remove needs to happen before being added to the appropriate pane
			dragging.remove();
			
			//Find out where to put the block
			WSingleFieldContainer<WBlock> target;
			if(currentHighlight != null)
				target = currentHighlight.getSlot();
			else
				target = getPaneAt(absX, absY);
			
			BlockDropAction action = (target == null?BlockDropAction.CANCEL:target.accepts(dragBlock));
			if(action == BlockDropAction.CANCEL)
			{
				dragCancel.returnBlock(dragBlock);
			}
			else if(action == BlockDropAction.ACCEPT)
			{
				if(target instanceof Slot)
				{
					if(((Slot) target).isMultiSlot)
					{
						//TODO size limits
						target.getChildren().insert(currentHighlight.getIndex(), dragBlock);
					}
					else
					{
						WBlock oldBlock = target.getChildren().get();
						if(oldBlock != null)
						{
							oldBlock.eject();
						}
						target.getChildren().set(dragBlock);
					}
				}
				else
				{
					target.getChildren().add(dragBlock);
					Point2D v = FXUtil.fromTo(rootPane, target.getUI(), new Point2D(absX-dragOffset.getX(), absY-dragOffset.getY()));
					dragBlock.setLocation((int)v.getX(), (int)v.getY());
				}
				assert dragBlock.getParent() == target : "Dropped block has the wrong parent: "+dragBlock.getParent();
			}
			else
				dragBlock.dispose();
			//Null out dragOffset after everything else because it is used by other calculations
			dragOffset = null;
		}
		if(highlights != null)
		{
			for(DropTargetHighlighter h : highlights)
			{
				h.destroy();
			}
		}
		highlights = null;
		currentHighlight = null;
	}
	
	private WPane getPaneAt(double absX, double absY)
	{
		//Need to return the panes in the correct z-order, which is not obvious.
		//Start by mapping candidate WPanes by their UIs
		HashMap<Pane, WPane> wpanesByUI = new HashMap<>();
		for(WNode p : children)
		{
			if(p instanceof WPane)
				wpanesByUI.put(p.getUI(), (WPane)p);
		}
		//Now, we need to select the candidate which is deepest in the scenegraph.
		//If multiple candidates are at the same depth, then they need to be selected by their position in the parent's child list.
//		Queue<Triplet<Node, Integer, Integer>> bfsQueue = new LinkedList<>(); 
//		bfsQueue.offer(new Triplet<>(rootPane, 0, 0));
//		while(!bfsQueue.isEmpty())
//		{
//			Triplet<Node, Integer, Integer> current = bfsQueue.poll();
//			if (FXUtil.isVisible(current.a) && FXUtil.fromTo(current.a, rootPane, current.a.getBoundsInLocal()).contains(absX, absY))
//			{
//				Triplet<WPane, Integer, Integer> mapValue = wpanesByUI.get(current.a); 
//				if(mapValue != null)
//				{
//					mapValue.b = current.b;
//					mapValue.c = current.c;
//				}
//				else
//				{
//					
//				}
//			}
//			//If it was not contained, then WPanes in this subtree will have depths left as -1.
//		}
		return wpanesByUI.get(findTopmost(rootPane, wpanesByUI, absX, absY));
		
	}
	/**Returns the topmost pane in the given subtree which is also a key in the given hashmap and contains the given position.*/
	private Pane findTopmost(Parent subtree, HashMap<Pane, ?> keyset, double absX, double absY)
	{
		//Only subtrees that contain the given point need to be considered.
		if (FXUtil.isVisible(subtree) && FXUtil.fromTo(subtree, rootPane, subtree.getBoundsInLocal()).contains(absX, absY))
		{
			//If this subtree is one of the targets, return it.
			if(keyset.containsKey(subtree))
				return (Pane) subtree;
			else //Otherwise, traverse
			{
				//Iterate children in reverse order, considering the topmost one first.
				ObservableList<Node> list = subtree.getChildrenUnmodifiable();
				for(int i = list.size()-1; i >= 0; i--)
				{
					Node cand = list.get(i);
					//Only Parents need to be considered.
					if(cand instanceof Parent)
					{
						Pane p = findTopmost((Parent)cand, keyset, absX, absY);
						if(p != null)
							return p;
					}
				}
			}
		}
		return null;
	}

	@Override
	public WContainer<WRoot> getParent()
	{
		return null;
	}
	@Override
	public PField<WNode> getChildren()
	{
		return children;
	}
	
	public TypeHierarchy getTypeHierarchy()
	{
		return typeHierarchy;
	}
	
	@Override
	public WRoot getRoot()
	{
		return this;
	}
	
	@Override
	public BlockDropAction accepts(WNode dragBlock)
	{
		return BlockDropAction.ACCEPT;
	}
	
	@Override
	public void handle(MouseEvent event)
	{
		//Note: JavaFX has bugs that cause a the real scene position to be different
		//from using the local-to-scene transform on event.getX()/getY().
		//Fortunately, we can get the real scene position directly, and it is not bugged.
		Point2D scenePt = new Point2D(event.getSceneX(), event.getSceneY());
		Point2D rootPt = rootPane.sceneToLocal(scenePt);
		double x = rootPt.getX();
		double y = rootPt.getY();
		
		if(event.getEventType() == MouseEvent.MOUSE_PRESSED)
		{
			WBlock blk = getBlockAt(new Point2D(x, y));
			if(blk != null)
			{
				Point2D p = blk.getUI().sceneToLocal(scenePt);
				if(event.getButton() == MouseButton.PRIMARY)
					dragStart(blk, p.getX(), p.getY(), x, y, false);
				else if(event.getButton() == MouseButton.SECONDARY)
					dragStart(blk, p.getX(), p.getY(), x, y, true);
				
			}
		}
		else if(event.getEventType() == MouseEvent.MOUSE_RELEASED)
		{
			dragEnd(x, y);
		}
		else if(event.getEventType() == MouseEvent.MOUSE_DRAGGED)
			drag(x, y);
	}
	
	@Override
	public WBlock getBlockAt(Point2D localPoint)
	{
		WPane pane = getPaneAt(localPoint.getX(), localPoint.getY());
		Pane ui = pane.getUI();
		Point2D panePoint = ui.sceneToLocal(rootPane.localToScene(localPoint.getX(), localPoint.getY()));
		return pane.getBlockAt(panePoint);
	}
	
	void pointerBlockAdded(WPointerBlock p)
	{
		if(connectorArrows.containsKey(p))
			new Throwable("Warning: "+p+" already added");
		FXArrow a = new FXArrow(p);
		rootPane.getChildren().addAll(a.getNodes());
		connectorArrows.put(p, a);
	}
	
	void pointerBlockRemoved(WPointerBlock p)
	{
		FXArrow a = connectorArrows.get(p);
		if(a == null)
			new Throwable("Warning: "+p+" already removed or not added");
		rootPane.getChildren().removeAll(a.getNodes());
	}
}
